/* -*- tab-width: 4; c-basic-offset: 2; indent-tabs-mode: nil -*- */

// Copyright (C) 2005 Sean M. Foy
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA

using System;
using System.Collections;
using System.Globalization;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Text;

namespace seanfoy {
  /// <summary>
  /// Some ASP.NET databindable controls (DataGrid in particular)
  /// are bad at handling lists whose elements have different
  /// structure. DefensiveDatasource shields the real data
  /// from the misbehavior of the databindable controls.
  /// </summary>
  /// <remarks>
  /// <para>Some ASP.NET web controls evidently care only about
  /// structural homogeneity among enumerated elements; as long
  /// as there are no properties on the first element for which
  /// some other element has no property by the same name, the
  /// web control's DataBind operation can succeed. Some other
  /// web controls evidently cache PropertyInfo instances
  /// obtained from the first element of the list, and try to
  /// invoke methods on those PropertyInfos for each element
  /// in the list. This effectively requires type-wise
  /// homogeneity for the elements of the list (a stronger
  /// requirement than structural homogeneity). DefensiveDatasource
  /// can deal with both problems.</para>
  /// <para>
  /// Was the .NET team trying to make us miserable?? What possible
  /// excuse could there be for getting so aggressive with list
  /// elements? Even if you turn off AutoGenerateColumns, it
  /// insists on probing each element in the list for every
  /// property of the first element.
  /// </para>
  /// </remarks>
  public class DefensiveDatasource : IEnumerable {
    /// <summary>
    /// Static methods for dynamic class
    /// generation using Reflection
    /// </summary>
    public class Chameleon {
      public static readonly string UNDERLYING_FIELD_NAME = "__underlying";
      protected static ModuleBuilder mb;

      static Chameleon() {
        AssemblyName an = new AssemblyName();
        an.Name = typeof(Chameleon).FullName + ".Chameleons";
        AssemblyBuilder ab = AppDomain.CurrentDomain.DefineDynamicAssembly(
          an,
          AssemblyBuilderAccess.Run);
        mb = ab.DefineDynamicModule("module");
      }

      /// <summary>
      /// For a given instance of a Chameleon-generated class
      /// <paramref name="hasUnderlying" />, return the <c>model</c>
      /// object upon which it was based.
      /// </summary>
      public static object getUnderlying(object hasUnderlying) {
        FieldInfo fi = hasUnderlying.GetType().GetField(UNDERLYING_FIELD_NAME, BindingFlags.NonPublic | BindingFlags.Instance);
        return fi.GetValue(hasUnderlying);
      }
      private static object[] noArgs = new object[] {};
      /// <summary>
      /// Quick and dirty functional abstractions to
      /// tide us over until we drop .NET 1.1 support
      /// and we get to live in
      /// Haskell^H^H^H^H^H^H^H^HLINQ full-time.
      /// </summary>
      public class FPUtils {
        public delegate object F(object x);
        public static ArrayList map(F f, IEnumerable l) {
          ArrayList result = new ArrayList();
          foreach (object i in l) result.Add(f(i));
          return result;
        }
        public static System.Collections.IDictionary zip(System.Collections.IEnumerable a, System.Collections.IEnumerable b) {
          System.Collections.Specialized.ListDictionary result =
            new System.Collections.Specialized.ListDictionary();
          IEnumerator i = a.GetEnumerator();
          IEnumerator j = b.GetEnumerator();
          while (i.MoveNext() && j.MoveNext()) {
            result.Add(i.Current, j.Current);
          }
          return result;
        }
        private class GetPropValueClosure {
          public PropertyInfo prop;
          public object _getPropValue(object owner) {
            return prop.GetValue(owner, noArgs);
          }
          public FPUtils.F getPropValue;
          public GetPropValueClosure(PropertyInfo prop) {
            this.prop = prop;
            this.getPropValue = new FPUtils.F(_getPropValue);
          }
        }
        public static Type[] comprehend(string propertyName, ParameterInfo[] parameterInfo) {
          PropertyInfo prop = typeof(ParameterInfo).GetProperty(propertyName);
          ArrayList result = FPUtils.map(new GetPropValueClosure(prop).getPropValue, parameterInfo);
          return (Type[])result.ToArray(typeof(Type));
        }
      }

      public static object proxyGet(object target, string propertyName, Type returnType, Type [] paramTypes, object [] paramValues) {
        // http://msdn2.microsoft.com/en-us/library/ek55ae84.aspx says
        // the DefaultBinder "does not process" the modifiers parameter.
        BindingFlags bindToAnything = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static;
        PropertyInfo pi = target.GetType().GetProperty(propertyName, bindToAnything, System.Type.DefaultBinder, returnType, paramTypes, null);
        MethodInfo mi = pi.GetGetMethod(true);
        return mi.Invoke(target, paramValues);
      }

      private static void _emitPushType(ILGenerator generator, object ot) {
        Type t = (Type)ot;
        //get a runtime representation of the value
        generator.Emit(OpCodes.Ldtoken, t);
        // convert to System.Type
        MethodInfo getTypeFromHandle = typeof(Type).GetMethod("GetTypeFromHandle");
        generator.Emit(OpCodes.Call, getTypeFromHandle);
      }
      private delegate void EmitPushSomething(ILGenerator generator, object o);
      private static EmitPushSomething emitPushType = new EmitPushSomething(_emitPushType);
      private static LocalBuilder emitAllocLocalArray(ILGenerator generator, Type localType, int length) {
        LocalBuilder result = generator.DeclareLocal(localType);
        generator.Emit(OpCodes.Ldc_I4, length);
        generator.Emit(OpCodes.Newarr, localType.GetElementType());
        generator.Emit(OpCodes.Stloc, result);
        return result;
      }
      private static LocalBuilder emitLocalArray(ILGenerator generator, Array a, EmitPushSomething emitPushSomething) {
        LocalBuilder result = emitAllocLocalArray(generator, a.GetType(), a.Length);
        //pack in the values
        for (int i = 0; i < a.Length; ++i) {
          generator.Emit(OpCodes.Ldloc, result);
          generator.Emit(OpCodes.Ldc_I4, i);
          emitPushSomething(generator, a.GetValue(i));
          generator.Emit(OpCodes.Stelem_Ref);
        }
        return result;
      }

      /// <summary>
      /// Dynamically generate a wrapper class for the given model type
      /// </summary>
      /// <param name="modelType">the basis for the wrapper</param>
      /// <param name="name">name for the generated type</param>
      /// <param name="propertyFilter">The names of properties that should be included in the wrapper, or null
      /// if you want all the properties included</param>
      protected static Type _wrap(Type modelType, string name, IDictionary propertyFilter) {
        //TODO: we should provide a means for filtering the properties so that the wrapper can
        // expose strict subsets of the properties of the underlying.
        TypeBuilder wrapper = mb.DefineType(name);

        // Our wrapper needs a reference to whatever it is wrapping
        FieldInfo underlying = wrapper.DefineField(UNDERLYING_FIELD_NAME, typeof(object), FieldAttributes.Private);

        // Let's build some getter properties. We don't need setters by the way.
        foreach (PropertyInfo pi in modelType.GetProperties()) {
          if (!(propertyFilter == null || propertyFilter.Contains(pi.Name))) continue;
          ParameterInfo[] parameterInfos = pi.GetIndexParameters();
          Type[] paramTypes = FPUtils.comprehend("ParameterType", parameterInfos);
          PropertyBuilder property = wrapper.DefineProperty(pi.Name, pi.Attributes, pi.PropertyType, paramTypes);
          MethodBuilder getter = wrapper.DefineMethod(
            "get" + pi.Name,
            MethodAttributes.Public,
            pi.PropertyType,
            paramTypes);
          ILGenerator getterBody = getter.GetILGenerator();

          // build paramTypes actual argument value
          //  we'll use a local variable for this
          LocalBuilder paramTypesLocal = emitLocalArray(getterBody, paramTypes, emitPushType);
          // build paramValues actual argument value
          LocalBuilder paramValuesLocal = emitAllocLocalArray(getterBody, typeof(object[]), parameterInfos.Length);
          for (int i = 0; i < parameterInfos.Length; ++i) {
            getterBody.Emit(OpCodes.Ldloc, paramValuesLocal);
            getterBody.Emit(OpCodes.Ldc_I4, i);
            getterBody.Emit(OpCodes.Ldarg, i + 1);
            if (parameterInfos[i].ParameterType.IsValueType) {
              getterBody.Emit(OpCodes.Box, parameterInfos[i].ParameterType);
            }
            getterBody.Emit(OpCodes.Stelem_Ref);
          }
          // target
          //   push the this pointer onto the stack for the following instance field access
          getterBody.Emit(OpCodes.Ldarg_0);
          //   push the field value onto the stack
          getterBody.Emit(OpCodes.Ldfld, underlying);
          // propertyName
          getterBody.Emit(OpCodes.Ldstr, pi.Name);
          // returnType
          emitPushType(getterBody, pi.PropertyType);
          // paramTypes
          getterBody.Emit(OpCodes.Ldloc, paramTypesLocal);
          // paramValues
          getterBody.Emit(OpCodes.Ldloc, paramValuesLocal);
          // OK, now do it.
          getterBody.Emit(OpCodes.Call, typeof(Chameleon).GetMethod("proxyGet"));
          if (pi.PropertyType.IsValueType) {
            getterBody.Emit(OpCodes.Unbox, pi.PropertyType);
            getterBody.Emit(OpCodes.Ldobj, pi.PropertyType);
          }
          getterBody.Emit(OpCodes.Ret);
          property.SetGetMethod(getter);
        }

        // We'll need a constructor in a moment...
        ConstructorBuilder constructor = wrapper.DefineConstructor(
          MethodAttributes.Public,
          CallingConventions.Standard,
          new Type[] {typeof(Object)});
        // parameter 0 is implicitly a this pointer
        constructor.DefineParameter(1, ParameterAttributes.None, "underlying");
        ILGenerator constructorBody = constructor.GetILGenerator();
        constructorBody.Emit(OpCodes.Ldarg_0);
        constructorBody.Emit(OpCodes.Ldarg_1);
        constructorBody.Emit(OpCodes.Stfld, underlying);
        constructorBody.Emit(OpCodes.Ret);
        return wrapper.CreateType();
      }

      /// <summary>
      /// Create a class name for a class
      /// generated from the given type
      /// using the given filter
      /// </summary>
      /// <param name="type">A class</param>
      /// <param name="filter">A dictionary with <seealso cref="String" />
      /// keys representing the properties that are wanted on the generated
      /// class</param>
      /// <returns>A sensible <seealso cref="System.Type.Name" />
      /// for the generated class</returns>
      public static string suggestName(Type type, IDictionary filter) {
        StringBuilder nom = new StringBuilder(type.Name);
        if (filter == null) return nom.ToString();
        nom.Append("_");
        int hc = 0;
        foreach (string fElementName in filter.Keys) {
          hc ^= fElementName.GetHashCode();
        }
        nom.Append(hc);
        return nom.ToString();
      }

      /// <summary>
      /// Instantiates a wrapper for the given object instance
      /// </summary>
      /// <remarks>
      /// The purpose of the wrapper is to provide access to a subset of
      /// the intersection over a variety of different types of their
      /// properties. The ASP.NET DataGrid control freaks out if you
      /// try to DataBind a set of objects when the first object in
      /// the collection has some field that is lacking in another of
      /// the objects (unless your set implements ITypedList). This
      /// method can be used to safely wrap your objects for use with
      /// DataGrid.DataBind.
      /// </remarks>
      /// <param name="model">The object to wrap</param>
      /// <param name="propertyFilter">The names of properties that should be included in the wrapper, or null
      /// if you want all the properties included</param>
      /// <param name="chameleonTypeName">The name of the emulating class. This method will return an instance
      /// of the emulating class. Use <c>null</c> if you would like distinct types (uniquely named) generated
      /// for each of the concrete types in the underlying collection</param>
      [MethodImpl(MethodImplOptions.Synchronized)]
      public static object Emulate(object model, IDictionary propertyFilter, string chameleonTypeName) {
        if (chameleonTypeName == null) chameleonTypeName = suggestName(model.GetType(), propertyFilter);

        if (mb.GetType(chameleonTypeName) == null) _wrap(model.GetType(), chameleonTypeName, propertyFilter);
        return mb.Assembly.CreateInstance(chameleonTypeName, false, BindingFlags.Default, null, new object[] {model}, CultureInfo.CurrentCulture, null);
      }

      public static object Emulate(object model, IDictionary propertyFilter) {
        return Emulate(model, propertyFilter, null);
      }

      public static object Emulate(object model) {
        return Emulate(model, null);
      }
    }


    /// <summary>
    /// Adapts an ordinary Enumerator to wrap collection elements
    /// using <see cref="Chameleon" />
    /// </summary>
    public class ChameleonEnumerator : IEnumerator {
      protected IEnumerator innerEnumerator;

      public ChameleonEnumerator(IEnumerable enumerable) : this(enumerable, null, null) {}

      public ChameleonEnumerator(IEnumerator enumerator) : this(enumerator, null, null) {}

      protected IDictionary propertyFilter = null;

      public ChameleonEnumerator(IEnumerator enumerator, IDictionary propertyFilter, String chameleonTypeName) {
        innerEnumerator = enumerator;
        this.propertyFilter = propertyFilter;
        this.chameleonTypeName = chameleonTypeName;
      }

      public ChameleonEnumerator(IEnumerable enumerable, IDictionary propertyFilter, String chameleonTypeName) {
        innerEnumerator = enumerable.GetEnumerator();
        this.propertyFilter = propertyFilter;
        this.chameleonTypeName = chameleonTypeName;
      }

      protected String chameleonTypeName;

      public object Current {
        get {
          return Chameleon.Emulate(innerEnumerator.Current, propertyFilter, chameleonTypeName);
        }
      }

      public bool MoveNext() {
        return innerEnumerator.MoveNext();
      }

      public void Reset() {
        innerEnumerator.Reset();
      }
    }

    private IEnumerable innerSource;
    private IDictionary propertyFilter = null;

    /// <summary>
    /// Creates an <see cref="System.Collections.IEnumerable" />
    /// instance that makes <see cref="ChameleonEnumerator" />s
    /// </summary>
    /// <param name="innerSource">A collection to adapt</param>
    /// <param name="propertyFilter">A dictionary whose keys list the properties that should be visible
    /// on the elements of the result, or <c>null</c> to let all the properties of each underlying element
    /// shine through<seealso cref="PropertySetIntersection" /></param>
    /// <remarks>
    /// Result will be homogeneous if <paramref name="propertyFilter" /> is non-null, and it may be heterogeneous if
    /// <paramref name="propertyFilter" /> is null.
    /// </remarks>
    public DefensiveDatasource(IEnumerable innerSource, IDictionary propertyFilter) {
      this.propertyFilter = propertyFilter;
      this.innerSource = innerSource;
    }

    /// <summary>
    /// Returns an IDictionary whose keys reflect (pun intended) the intersection of
    /// the sets of property names on each element of <paramref name="instances" />.
    /// </summary>
    public static IDictionary PropertySetIntersection(IEnumerable instances) {
      IDictionary propertyFilter = new Hashtable();
      IEnumerator enumerator = instances.GetEnumerator();
      if (enumerator.MoveNext()) {
        foreach (PropertyInfo pi in enumerator.Current.GetType().GetProperties()) {
          propertyFilter.Add(pi.Name, 0);
        }
        while (enumerator.MoveNext()) {
          IDictionary temp = new Hashtable(propertyFilter);
          foreach (string piName in temp.Keys) {
            if (enumerator.Current.GetType().GetProperty(piName) == null) {
              propertyFilter.Remove(piName);
            }
          }
        }
      }
      return propertyFilter;
    }

    /// <summary>
    /// Returns the most derived Type that is a supertype of all the elements
    /// in the source, where supertype is reflexive.
    /// </summary>
    public static Type NearestCommonAncestorType(IEnumerable source) {
      IEnumerator enumerator = source.GetEnumerator();
      if (!enumerator.MoveNext()) return typeof(Object);

      Type result = enumerator.Current.GetType();
      while (enumerator.MoveNext()) {
        while (!result.IsAssignableFrom(enumerator.Current.GetType())) {
          result = result.BaseType;
        }
      }

      return result;
    }

    public IEnumerator GetEnumerator() {
      string superTypeName = NearestCommonAncestorType(innerSource).Name;
      return new ChameleonEnumerator(innerSource, propertyFilter, superTypeName);
    }
  }
}
